﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace LunarSF.SHomeWorkshop.LunarMarkdownEditor.Utils
{
    public partial class RevealJsTools
    {
        public class RevealJsSlide : RevealJsInnerSlide
        {
            private readonly List<RevealJsInnerSlide> innerSlides = new List<RevealJsInnerSlide>();

            public List<RevealJsInnerSlide> InnerSlides { get { return innerSlides; } }

            /// <summary>
            /// 此方法用于根据 样式表、特效表、内容文本生成对应的 Html 片段。
            /// 如果有内嵌的子幻灯片，只编译子幻灯片为 Html 片段集；如果没有子幻灯片，直接编译其中的内容。
            /// </summary>
            public string Html
            {
                get
                {
                    return CompileSlide(this);
                }
            }

            /// <summary>
            /// 将幻灯片内容编译
            /// </summary>
            /// <returns></returns>
            public static string CompileSlide(RevealJsSlide slide)
            {
                if (slide == null) return "";
                if (string.IsNullOrWhiteSpace(slide.ContentMarkdownText.ToString()) &&
                    string.IsNullOrWhiteSpace(slide.NoteLinesText.ToString()) &&
                    slide.InnerSlides.Count <= 0) return "";

                // 基本思路：
                //     1. 处理幻灯片的样式表、特效设置
                //     2. 处理幻灯片的内容

                StringBuilder sb = new StringBuilder();
                sb.Append("<section ");
                // 1. 处理幻灯片的样式表、特效设置（即一些关于幻灯片的选项）

                var styleText = "";
                var regSimpleStyle = new Regex(@"(?<=(^[ 　\t]{0,}?[\$￥])).*$");
                var matchSimpleStyle = regSimpleStyle.Match("$" + slide.HeaderStyleText);
                if (matchSimpleStyle.Success)
                {
                    styleText = CustomMarkdownSupport.ConvertToCssStyle(matchSimpleStyle);

                    sb.Append(styleText);
                }

                sb.Append(slide.OptionsAttributeText);

                sb.Append(">");

                // 2. 处理幻灯片的内容
                if (slide.InnerSlides.Count > 0)
                {
                    foreach (var iSlide in slide.InnerSlides)
                    {
                        sb.Append(CompileInnerSlide(iSlide));
                    }
                }
                else
                {
                    sb.Append(CompileSlideContent(slide.ContentMarkdownText.ToString()));
                }

                // 这里添加 Note ——用于演讲者模式
                if (string.IsNullOrWhiteSpace(slide.NoteLinesText.ToString()) == false)
                {
                    sb.Append("<aside class=\"notes\">\r\n");
                    sb.Append(slide.NoteLinesText);
                    sb.Append("</aside>");
                }

                sb.Append("</section>");

                return sb.ToString();
            }

            /// <summary>
            /// 将从方块区域获得的内容转换成 html 片段。
            /// </summary>
            /// <param name="iSlide"></param>
            /// <returns></returns>
            private static string CompileInnerSlide(RevealJsInnerSlide iSlide)
            {
                if (iSlide == null) return "";
                if (string.IsNullOrWhiteSpace(iSlide.ContentMarkdownText.ToString()) &&
                    string.IsNullOrWhiteSpace(iSlide.NoteLinesText.ToString())) return "";

                StringBuilder sb = new StringBuilder();

                sb.Append("<section ");

                // 选项
                sb.Append(iSlide.OptionsAttributeText);

                sb.Append(">");

                var content = CompileSlideContent(iSlide.ContentMarkdownText.ToString());
                sb.Append(content);

                // 这里添加 Note ——用于演讲者模式
                if (string.IsNullOrWhiteSpace(iSlide.NoteLinesText.ToString()) == false)
                {
                    sb.Append("<aside class=\"notes\">\r\n");
                    sb.Append(iSlide.NoteLinesText);
                    sb.Append("</aside>");
                }

                sb.Append("</section>");

                return sb.ToString();
            }

            /// <summary>
            /// 编译幻灯片的内容文本为 Html 片段。（不含首尾的 &lt;section&gt; 和 &lt;/section&gt; 标签对。）
            /// </summary>
            /// <param name="content"></param>
            /// <returns></returns>
            private static string CompileSlideContent(string contentText)
            {
                if (string.IsNullOrWhiteSpace(contentText))
                    return "";

                // 基本思路：
                // 先编译块级元素，再考虑行内元素。

                var md = new MarkdownSharp.Markdown();

                var sb = new StringBuilder();
                contentText = CustomMarkdownSupport.FormatMarkdownText(contentText);

                #region 处理 Setext 样式的伪标题（虚拟标题，virtual title）、冒号注释和普通段落
                var lines = contentText.Split(new string[] { "\r\n", }, StringSplitOptions.None);

                var regVirtualTitle = new Regex(@"^\^[#＃]{1,6}");
                bool ignoreNextMarkLine = false;
                var trimChars1 = new char[] { ' ', '　', '\t', '^', };
                var trimChars2 = new char[] { ' ', '　', '\t', };
                for (int i = 0; i < lines.Length; i++)
                {
                    if (ignoreNextMarkLine)
                    {
                        ignoreNextMarkLine = false;  // 跳过 Setext 样式的伪标题标记行。
                        continue;
                    }

                    if (string.IsNullOrWhiteSpace(lines[i]))
                    {
                        sb.Append(lines[i] + "\r\n");
                        continue;
                    }

                    var next = i + 1;
                    if (next >= lines.Length)
                    {
                        sb.Append(lines[i]);
                        sb.Append("\r\n");
                        continue;
                    }
                    else
                    {
                        if (CustomMarkdownSupport.IsHorizontalLineText(lines[next]))   // 5级伪标题
                        {
                            sb.Append("<h5>" + CompileFragments(lines[i]) + "</h5>\r\n");
                            ignoreNextMarkLine = true;
                            continue;
                        }

                        if (CustomMarkdownSupport.IsHeaderMarkLine(lines[next]))    // 3级伪标题
                        {
                            sb.Append("<h3>" + CompileFragments(lines[i]) + "</h3>\r\n");
                            ignoreNextMarkLine = true;
                            continue;
                        }

                        var matchVirtualTitle = regVirtualTitle.Match(lines[i]);
                        if (matchVirtualTitle.Success)
                        {
                            var level = matchVirtualTitle.Value.Trim(trimChars1).Length;
                            sb.Append($"<h{level}>" + lines[i].Substring(matchVirtualTitle.Length) + $"</h{level}>");
                            sb.Append("\r\n");
                            continue;
                        }

                        if (lines[i].StartsWith(":") || lines[i].StartsWith("："))
                        {
                            var visualCommentRegex = new Regex(@"^[:：][ 　\t]{0,}?[\[【][ 　\t]{0,}?[A-Za-z\-_0-9\u4E00-\u9FA5]{0,}?[ 　\t]{0,}?[\]】]");  // 冒号开头的注释

                            // 冒号注释开头以一对方括号括起来的字符串表示当前段落的 class。它们通常应该是 RevealJSTools.FragmentAction 中定义的几种 css class 字符串。
                            var matchVisualComment = visualCommentRegex.Match(lines[i]);
                            if (matchVisualComment.Success)
                            {
                                var userInputedClassname = matchVisualComment.Value.Trim(new char[] { ' ', '　', '\t', '[', ']', ':', '：', '【', '】', });
                                var className = FragmentAction.GetFullActionClassName(userInputedClassname);
                                if (className == "[UnKnown]")
                                {
                                    // 如果填写的 className 不正确，视为普通方括号对。
                                    sb.Append($"<p class=\"{userInputedClassname}\">\r\n");
                                }
                                else
                                {
                                    // 第一个方括号对用来设置整段的特效动作。
                                    sb.Append("<p class=\"" + className + "\">\r\n");  // 整段这里所有特效都有用。
                                }
                                var tail = lines[i].Substring(matchVisualComment.Length);
                                sb.Append(CompileVisualCommentSpans(tail));
                                sb.Append("</p>\r\n");
                            }
                            else
                            {
                                var styleText = "";
                                var regSimpleStyle = new Regex(@"(?<=(^[:：][ 　\t]{0,}?[\[【][ 　\t]{0,}?[\$￥])).*?(?=([\]】]))");
                                var matchSimpleStyle = regSimpleStyle.Match(lines[i]);
                                if (matchSimpleStyle.Success)
                                {
                                    styleText = CustomMarkdownSupport.ConvertToCssStyle(matchSimpleStyle);

                                    sb.Append($"<p {styleText}>\r\n");
                                    sb.Append(CompileVisualCommentSpans(lines[i].Substring(matchSimpleStyle.Index + matchSimpleStyle.Length + 1).Trim(trimChars2)));
                                    sb.Append("</p>\r\n");
                                }
                                else
                                {
                                    // 只需要考虑内部的片段即可。写法是 [className:文本内容]
                                    sb.Append("<p>\r\n");
                                    sb.Append(CompileVisualCommentSpans(lines[i].Substring(1)));
                                    sb.Append("</p>\r\n");
                                }
                            }
                            continue;
                        }

                        // 普通段落行
                        sb.Append(/*"<p>" + */CompileFragments(lines[i])/* + "</p>"*/);  // 注意：这里首尾不能加 <p>...</p> 标签——否则后续列表项、二维文字表啥的都没用。
                        sb.Append("\r\n");
                        continue;
                    }
                }
                contentText = sb.ToString();

                #endregion

                #region 二维文字表
                lines = contentText.Split(new string[] { "\r\n", }, StringSplitOptions.None);
                sb = new StringBuilder();

                var sbTable = new StringBuilder();
                var inTextTable = false;
                foreach (var line in lines)
                {
                    if (CustomMarkdownSupport.IsTableRow(line) || CustomMarkdownSupport.IsTableCaptionSplittor(line))
                    {
                        sbTable.Append(line);
                        sbTable.Append("\r\n");
                        inTextTable = true;
                    }
                    else
                    {
                        if (inTextTable)
                        {
                            var tableInfo = CustomMarkdownSupport.CompileTextTable(sbTable.ToString().Split(new string[] { "\r\n", }, StringSplitOptions.None), ref md);
                            if (tableInfo.Count > 0)  // 实际上只应该包含一个二维文字表
                            {
                                sb.Append(tableInfo[0].TableHtmlText);
                                sb.Append("\r\n");
                            }
                            sbTable = new StringBuilder();
                        }

                        sb.Append(line);
                        sb.Append("\r\n");
                        inTextTable = false;
                    }
                }

                if (inTextTable)  // 如果最后一行还是表格行。
                {
                    var tableInfo = CustomMarkdownSupport.CompileTextTable(sbTable.ToString().Split(new string[] { "\r\n", }, StringSplitOptions.None), ref md);
                    if (tableInfo.Count > 0)  // 实际上只应该包含一个二维文字表
                    {
                        sb.Append(tableInfo[0].TableHtmlText);
                        sb.Append("\r\n");
                    }
                }
                contentText = sb.ToString();
                #endregion

                #region 代码块（含树型文字表）——要抢先处理

                lines = contentText.Split(new string[] { "\r\n", }, StringSplitOptions.None);

                var preLineIsCodeBlock = false;
                foreach (var line in lines)
                {
                    var lineText = line;
                    if (line.StartsWith("!") || line.StartsWith("！"))  // 树型文字表视为代码块
                    {
                        lineText = "    " + line;
                    }

                    if (CustomMarkdownSupport.IsCodeBlockLine(lineText, out string codeContent))  // 这个方法会自动进行 Html 转义。
                    {
                        if (preLineIsCodeBlock == false)
                        {
                            // 这行是第一个代码块行，但前一行不是代码块行
                            sb.Append("<pre><code>");
                        }

                        sb.Append(codeContent);
                        sb.Append("\r\n");

                        preLineIsCodeBlock = true;
                    }
                    else
                    {
                        if (preLineIsCodeBlock)
                        {
                            sb.Append("</code></pre>\r\n");
                        }

                        sb.Append(CompileFragments(lineText));   // 普通段落中以行内代码块标记要显示为 片段 的文本。
                        sb.Append("\r\n");

                        preLineIsCodeBlock = false;
                    }
                }
                #endregion

                #region 试题

                // TODO：暂时未实现 reveal.js 演示试题，目前看来，reveal.js 显示试题时，不能直接借用之前的试题编译代码。
                //     只能另外实现一套。
                //         填空题，最简单：直接替换掉头部即可。填空项直接把实心方括号替换成对应的 html 标签即可。
                //         选择题，难度较大：要考虑四个选项和答案。
                //         材料解析题，难度最大。
                
                // 试题在每个幻灯片中处理会导致一个问题：编号中断（每张幻灯片都会重新计算编号。
                // 现移动到 RevealJsTools.js 中的 Show() 方法里。
                // contentText = Question.ConvertQuestionsToHtml(contentText, true);  //先将试题文本转换为html标签。

                #endregion

                #region 列表、引用块、链接等可以让 MarkdownSharp 处理的元素

                // 行内代码块之前已处理过。
                contentText = md.Transform(contentText);

                // 列表项均添加 class="fragments"

                contentText = contentText.Replace("<li>", "<li class=\"fragment\">");  // 这个不会影响代码块中的 <li></li> ——因为代码块中已经进行转义了。

                #endregion

                return contentText;
            }

            /// <summary>
            /// 将冒号开头的注释中 [xxx:yyy] 这样的片段写成 reveal.js 能识别的 html 片段。
            /// 注意：冒号注释行中不能支持双反引号这种——同时支持必然产生冲突。
            /// </summary>
            /// <param name="v"></param>
            /// <returns></returns>
            private static string CompileVisualCommentSpans(string v)
            {
                var pieces = Utils.Text.SplitToPiecesByRegex(@"\[[^\]]*?\]\(.*\)", v);  // 防止链接被当成文本片段。

                var sb = new StringBuilder();
                foreach (var p in pieces)
                {
                    if (p.IsMartchText)
                    {
                        sb.Append(p.SourceText);
                        continue;
                    }
                    else
                    {
                        sb.Append(CompileVisualCommentPieces(p.SourceText));
                        continue;
                    }
                }
                return sb.ToString();
            }

            /// <summary>
            /// 为什么要两层呢？这是为了防止链接中的方括号被当成文本片段。
            /// </summary>
            private static string CompileVisualCommentPieces(string v)
            {
                if (string.IsNullOrWhiteSpace(v)) return "";

                var pieces = Utils.Text.SplitToPiecesByRegex(@"[【\[].*?[\]】]", v);

                var trimChars = new char[] { ' ', '　', '\t', '[', ']', };
                var trimRight = new char[] { ' ', '　', '\t', ']', };
                var sb = new StringBuilder();
                foreach (var piece in pieces)
                {
                    if (piece.IsMartchText)
                    {
                        var index = piece.SourceText.ToLower().Replace("：", ":").IndexOf(":");
                        if (index >= 0)
                        {
                            var userInputedSpanClassName = piece.SourceText.Substring(0, index).ToLower().Trim(trimChars);
                            var content = piece.SourceText.Substring(index + 1).TrimEnd(trimRight);

                            var spanClassName = FragmentAction.GetFullActionClassName(userInputedSpanClassName);
                            if (string.IsNullOrWhiteSpace(spanClassName))
                            {
                                sb.Append("<span style=\"border-bottom:0.1em solid;\"> <span class=\"fragment\">" + content + "</span> </span>");
                            }
                            else if (spanClassName == "[UnKnown]")
                            {
                                sb.Append($"<span style=\"border-bottom:0.1em solid;\"> <span class=\"{userInputedSpanClassName}\">" + content + "</span> </span>");
                            }
                            else
                            {
                                // 在文本片段上，有些特效（grow/shink）是不起作用的；另外一些特效（fade-left/fade-right/fade-up/fade-down）效果完全一样。
                                // 所以调用 FragmentAction.GetFullSpanActionClassName() 方法干脆合并为一种。
                                sb.Append("<span style=\"border-bottom:0.1em solid;\"> <span class=\"" + FragmentAction.GetFullSpanActionClassName(spanClassName) + "\">" + content + "</span> </span>");
                            }
                        }
                        else
                        {
                            //省略 class 直接按这个来。
                            sb.Append("<span style=\"border-bottom:0.1em solid;\"> <span class=\"fragment\">" + piece.SourceText.Trim(trimChars) + "</span> </span>");
                        }
                    }
                    else  // 未以 [] 扩起来的文本。
                    {
                        sb.Append(piece.SourceText);
                    }
                }
                return sb.ToString();
            }

            /// <summary>
            /// 注意：冒号开头的注释行中不能运行两个反引号这种片段——否则一定会产生冲突。
            /// 将找到的用 反引号对 表示的行内代码块转换成 &lt;code class="fragment"&gt; 和 &lt;/code&gt; 这样子。
            /// </summary>
            /// <param name="line"></param>
            /// <returns></returns>
            private static string CompileFragments(string line)
            {
                if (string.IsNullOrWhiteSpace(line)) return "";

                var pieces = Utils.Text.SplitToPiecesByRegex("`", line);

                var sb = new StringBuilder();
                bool isOpened = false;
                foreach (TextPiece piece in pieces)
                {
                    if (piece.IsMartchText == false)
                    {
                        sb.Append(piece.SourceText);
                        continue;
                    }

                    if (isOpened == false)
                    {
                        sb.Append("<span style='border-bottom:0.1em solid;'> <span class=\"fragment\"><b>");
                        isOpened = true;
                        continue;
                    }
                    else
                    {
                        sb.Append("</b></span> </span>");
                        isOpened = false;
                        continue;
                    }
                }

                return sb.ToString();
            }
        }
    }
}
